package main;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.rmi.AccessException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.UnmarshalException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter.DEFAULT;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.SystemUtils;

import comunicacao.GestorZooKeeper;
import comunicacao.ReceiveMessageInterface;
import comunicacao.primaryDBInterface;
import entities.CachedTeatro;
import entities.ConfigInfo;
import entities.Lugar;
import entities.Range;
import entities.Request;
import entities.ServerInfo;
import entities.Teatro;

/**
 * Gerenciador da base de dados que controla as operações requisitadas
 * sobre os teatros
 * 
 * @author Leonardo Lino Nº45340
 * @author Diogo Pires Nº40351
 */
public class DBmanager extends java.rmi.server.UnicastRemoteObject implements
		ReceiveMessageInterface, Runnable {

	private GestorZooKeeper zKeeper;
	private final int id;
	private ServerInfo leaderInfo;
	private int idReplic;
	private int idCoordinator;
	private final int CACHE_TIME_LIMIT;
	private final int CACHE_MAX_AGE;
	private final String CURRENT_DIR;
	private final String RUNNING_LOCATION;
	private final String OS;
	private String eol = System.lineSeparator();
	private int thisPort;
	private int rows;
	private int columns;
	private int numtheaters;
	private boolean disabled;
	private boolean fileOverride;
	private String thisAddress;
	private String baseLocation;
	private Registry registry;
	private FileOperator fileOps;
	private FileOperator fileOpsReplic;
	private LinkedHashMap<Integer, Lugar> seatsMap;
	private LinkedHashMap<String, Integer> theatersMap;
	private LinkedHashMap<Integer, CachedTeatro> cache;
	private final String dbRegistryName;
	private ArrayList<String> theaterNamesList;
	private Object signal;
	private Range serverRange;
	private Range replicRange;
	private Range coordinatorRange;
	
	public DBmanager(ConfigInfo config, Object signal, GestorZooKeeper zKeeper, ServerInfo leaderInfo) throws RemoteException {
		int cacheLimit = 0;
		disabled = false;
		OS = SystemUtils.OS_NAME;
		
		CURRENT_DIR = System.getProperty("user.dir") + File.separator;

		if(SystemUtils.IS_OS_LINUX){
			String temp = FileOperator.path(new File(this.getClass().getProtectionDomain().getCodeSource().getLocation().getPath()), true,true);
			RUNNING_LOCATION = temp.replace("%20"," ");
		}else if(SystemUtils.IS_OS_WINDOWS)
			RUNNING_LOCATION = CURRENT_DIR;
		else
			RUNNING_LOCATION = FileOperator.path(new File(this.getClass().getProtectionDomain().getCodeSource().getLocation().getPath()), true,true);
		
		
		fileOverride = config.isFileOverride();
		thisAddress = config.getThisAddress();
		baseLocation = config.getBaseLocation();
		thisPort = config.getThisPort();
		rows = config.getRows();
		columns = config.getColumns();
		numtheaters = config.getNumtheaters();
		cacheLimit = config.getCacheLimit();
		thisAddress = config.getThisAddress();
		id = config.getId();
		
		this.leaderInfo = leaderInfo;
		
		dbRegistryName = "dbServer" + Integer.toString(id);
		
		this.signal = signal;
		this.zKeeper = zKeeper;
		
		CACHE_TIME_LIMIT = cacheLimit;
		CACHE_MAX_AGE = CACHE_TIME_LIMIT * 3;

		fileOps = new FileOperator(baseLocation);
		fileOpsReplic = new FileOperator(baseLocation + "replicStorage/");

		theaterNamesList = new ArrayList<String>();
		
		// generate theaters names
		genTheaterslist();		
		// generate theaters translation hash
		genTheatersHash();
		// generate seats translation hash
		genSeatsHash();
		// inicialize cache
		cache = new LinkedHashMap<Integer, CachedTeatro>(100);

		Executors.newScheduledThreadPool(1).scheduleWithFixedDelay(this, 0, 10,
				TimeUnit.SECONDS);

		try {
			// create the registry and bind the name and object.
			registry = LocateRegistry.createRegistry(thisPort);
			registry.rebind(dbRegistryName, this);
		} catch (RemoteException e) {
			throw e;
		}
		
		synchronized (signal) {
			signal.notifyAll();
		}
	}

	public ArrayList<String> listTheaters() throws RemoteException {

		//System.out.println("Received: request for list of Theaters");
		
		/*
		ArrayList<String> theaters = new ArrayList<String>();
		String str;

		System.out.println("Received: request for list of Theaters");

		if (theaterNamesList == null)
			try {
				for (int i = 1; i <= numtheaters; i++) {
					if ((str = fileOps.getTheaterName(i)) != null)
						theaters.add(str);
				}
				theaterNamesList = theaters;
			} catch (IOException e) {
				e.printStackTrace();
			}
			*/
		
		/*if(theaterNamesList == null)
			System.out.println("sending NULL");*/

		return theaterNamesList;
	}
	
	public boolean removeTheater(String theaterName, boolean inReplic) throws RemoteException {
		int teatroID;
		boolean result = true;
		
		//System.out.println("Received: removal request of theater " + theaterName + " inReplic=" + inReplic);
		
		try {
			teatroID = theatersMap.get(theaterName);
			
			if(inReplic)
				result = fileOpsReplic.deleteTheater(teatroID);
			else
				result = fileOps.deleteTheater(teatroID);
		} catch (NullPointerException e) {
			result = false;
		}
		
		return result;
	}

	public Teatro getTheaterInReplic(String theaterName) throws RemoteException {
		
		Teatro teatro = null;
		int teatroID;
		CachedTeatro cachedObject;

		try {
			teatroID = theatersMap.get(theaterName);
			synchronized (this) {
				if ((cachedObject = cache.get(teatroID)) != null) {
					cachedObject.setTimeUpdated(System.currentTimeMillis());
					return cachedObject.getTeatro();
				}
			}
		} catch (NullPointerException e) {
			return null;
		}

		try {
			teatro = fileOpsReplic.getTheater(teatroID, columns);
		} catch (IOException e) {
			e.printStackTrace();
			return null;
		}

		if (teatro != null)
			synchronized (this) {
				cache.put(teatro.id,
						new CachedTeatro(teatro, System.currentTimeMillis(), true));
			}

		return teatro;
	}
	
	public Teatro getTheater(String theaterName) throws RemoteException {

		Teatro teatro = null;
		int teatroID;
		CachedTeatro cachedObject;

		//System.out.println("Received: request for " + theaterName);

		try {
			teatroID = theatersMap.get(theaterName);
			synchronized (this) {
				if ((cachedObject = cache.get(teatroID)) != null) {
					cachedObject.setTimeUpdated(System.currentTimeMillis());
					return cachedObject.getTeatro();
				}
			}

		} catch (NullPointerException e) {
			return null;
		}

		try {
			teatro = fileOps.getTheater(teatroID, columns);
		} catch (IOException e) {
			e.printStackTrace();
			return null;
		}

		if (teatro != null)
			synchronized (this) {
				cache.put(teatro.id,
						new CachedTeatro(teatro, System.currentTimeMillis(), false));
			}

		return teatro;
	}

	public boolean reserveSeat(String theaterName, int lugarID, int userID, String timestamp, boolean inReplic)
			throws RemoteException {

		int teatroID;
		boolean result = false;
		CachedTeatro cachedObject;

		//System.out.println("Received: reservation request from user-" + userID
		//		+ " of seat-" + lugarID + " within " + theaterName);

		try {
			teatroID = theatersMap.get(theaterName);
		} catch (NullPointerException e) {
			return false;
		}

		Lugar lugar = seatsMap.get(lugarID);
		if (lugar == null)
			return false;

		synchronized (this) {
			if ((cachedObject = cache.get(teatroID)) != null) {
				Lugar lugarCached = cachedObject.getTeatro().getLugares()
						.get(lugarID - 1);

				if (lugar.row == lugarCached.row
						&& lugar.column == lugarCached.column
						&& lugarCached.getState() == Lugar.STATE_FREE) {
					lugarCached.setState(Lugar.STATE_RESERVED);
					lugarCached.setUserID(userID);
					lugarCached.setTimestamp(timestamp);
					cachedObject.getTeatro().lugaresReservados++;
					cachedObject.setTimeUpdated(System.currentTimeMillis());
					cachedObject.setAltered();
					
					if(!inReplic)
						broadcastToReplics(new Request(Request.TYPE_PURCHASE, theaterName, lugarID, userID, timestamp, true));
					
					return true;
				} else {
					//System.out.println("FALSE CACHE");
					cachedObject.setTimeUpdated(System.currentTimeMillis());
					return false;
				}
			}
		}

		try {
			if(inReplic)
				result = fileOpsReplic.reserveSeat(teatroID, userID, lugar.row,
						lugar.column, timestamp);
			else
				result = fileOps.reserveSeat(teatroID, userID, lugar.row,
						lugar.column, timestamp);
		} catch (IOException e) {
			e.printStackTrace();
		}

		if(result)
			if(!inReplic)
				broadcastToReplics(new Request(Request.TYPE_PURCHASE, theaterName, lugarID, userID, timestamp, true));
		
		return result;
	}

	public boolean removeSeatReserve(String theaterName, int lugarID, int userID, boolean inReplic)
			throws RemoteException {

		int teatroID;
		boolean result = false;
		CachedTeatro cachedObject;

		//System.out.println("Received: reservation removal request of seat-"
		//		+ lugarID + " within " + theaterName + " ocupied by user-"
		//		+ userID);

		try {
			teatroID = theatersMap.get(theaterName);
		} catch (NullPointerException e) {
			return false;
		}

		Lugar lugar = seatsMap.get(lugarID);
		if (lugar == null)
			return false;

		synchronized (this) {
			if ((cachedObject = cache.get(teatroID)) != null) {
				Lugar lugarCached = cachedObject.getTeatro().getLugares()
						.get(lugarID - 1);

				if (lugar.row == lugarCached.row
						&& lugar.column == lugarCached.column
						&& lugarCached.getState() == Lugar.STATE_RESERVED
						&& lugarCached.getUserID() == userID) {
					lugarCached.setState(Lugar.STATE_FREE);
					lugarCached.setUserID(0);
					lugarCached.setTimestamp(null);
					cachedObject.getTeatro().lugaresReservados--;
					cachedObject.setTimeUpdated(System.currentTimeMillis());
					cachedObject.setAltered();
					
					if(!inReplic)
						broadcastToReplics(new Request(Request.TYPE_PURCHASE, theaterName, lugarID, userID, null, true));
					
					return true;
				} else {
					cachedObject.setTimeUpdated(System.currentTimeMillis());
					return false;
				}
			}
		}

		try {
			if(inReplic)
				result = fileOpsReplic.removeSeatReserve(teatroID, userID, lugar.row,
						lugar.column);
			else
				result = fileOps.removeSeatReserve(teatroID, userID, lugar.row,
						lugar.column);
		} catch (IOException e) {
			e.printStackTrace();
		}

		if(result)
			if(!inReplic)
				broadcastToReplics(new Request(Request.TYPE_PURCHASE, theaterName, lugarID, userID, null, true));
		
		return result;
	}

	public boolean purchaseSeat(String theaterName, int lugarID, int userID, boolean inReplic)
			throws RemoteException {

		boolean result = false;
		int teatroID;
		CachedTeatro cachedObject;

		//System.out.println("Received: purchase request from user-" + userID
		//		+ " of seat-" + lugarID + " within " + theaterName);

		try {
			teatroID = theatersMap.get(theaterName);
		} catch (NullPointerException e) {
			return false;
		}

		Lugar lugar = seatsMap.get(lugarID);
		if (lugar == null)
			return false;

		synchronized (this) {
			if ((cachedObject = cache.get(teatroID)) != null) {
				Lugar lugarCached = cachedObject.getTeatro().getLugares()
						.get(lugarID - 1);

				if (lugar.row == lugarCached.row
						&& lugar.column == lugarCached.column) {

					if (lugarCached.getState() == Lugar.STATE_FREE
							|| (lugarCached.getState() == Lugar.STATE_RESERVED && lugarCached
									.getUserID() == userID)) {
						if(lugarCached.getState() == Lugar.STATE_RESERVED)
							cachedObject.getTeatro().lugaresReservados--;
						lugarCached.setState(Lugar.STATE_BOUGHT);
						lugarCached.setUserID(userID);
						lugarCached.setTimestamp(null);
						cachedObject.getTeatro().lugaresOcupados++;
						cachedObject.setTimeUpdated(System.currentTimeMillis());
						cachedObject.setAltered();
						
						if(!inReplic)
							broadcastToReplics(new Request(Request.TYPE_PURCHASE, theaterName, lugarID, userID, null, true));
						
						return true;
					}else{
						cachedObject.setTimeUpdated(System.currentTimeMillis());
						return false;
					}
				} else {
					cachedObject.setTimeUpdated(System.currentTimeMillis());
					return false;
				}
			}
		}

		try {
			if(inReplic)
				result = fileOpsReplic.purchaseSeat(teatroID, userID, lugar.row,
						lugar.column);
			else
				result = fileOps.purchaseSeat(teatroID, userID, lugar.row,
						lugar.column);
		} catch (IOException e) {
			e.printStackTrace();
		}

		if(result)
			if(!inReplic)
				broadcastToReplics(new Request(Request.TYPE_PURCHASE, theaterName, lugarID, userID, null, true));
			
		
		return result;
	}

	//TODO
	public boolean broadcastToReplics(Request request){
		boolean result = true;
		
		/*if(idReplic > 0){
			String targetBindName = "dbServer" + idReplic;
			Registry replicRegistry = LocateRegistry.getRegistry(host, port);
		}*/
		
		return result;
	}
	
	public boolean falha(boolean failureMode, boolean force) throws RemoteException {
		if(failureMode)
			System.out.println("Received: system failure request");
		//else
			//System.out.println("Received: database shutdown request");
		
		shutDownServerRegistry(failureMode,force);
		return disabled = true;
	}

	public boolean isInFailure() {
		return disabled;
	}

	public boolean restart() throws RemoteException {
		System.out.println("Received: system restart request");
		disabled = false;
		restartServerRegistry();
		return (!disabled);
	}

	private boolean genTheater(int teatroID, int rows, int columns,
			boolean overwrite) {
		boolean result = false;

		try {
			result = fileOps.genTheater(teatroID, rows, columns, overwrite);
		} catch (IOException e) {
		}
		return result;
	}

	public boolean writeTheater(Teatro teatro, boolean inReplic) throws RemoteException{
		
		//System.out.println("Received: write theater request of theater " + teatro.getNome() + " inReplic=" + inReplic);
		
		try {
			if(inReplic)
				fileOpsReplic.writeTheater(teatro);
			else
				fileOps.writeTheater(teatro);
		} catch (IOException e) {
			System.out.println("Error writing theater to file");
			return false;
		}
		return true;
	}

	private void genSeatsHash() {
		seatsMap = new LinkedHashMap<Integer, Lugar>(500);
		int id = 1;

		for (int i = 0; i < rows; i++) {
			for (int j = 1; j <= columns; j++) {
				Lugar lugar = new Lugar(id, i, j, Lugar.STATE_FREE);
				seatsMap.put(id, lugar);
				id++;

			}
		}
	}

	private void genTheatersHash() {
		theatersMap = new LinkedHashMap<String, Integer>(500);
		String name;

		for (int i = 1; i <= numtheaters; i++) {
			name = "teatro" + Integer.toString(i);
			theatersMap.put(name, i);
		}
	}
	
	private void genTheaterslist() {
		for (int i = 1; i <= numtheaters; i++) {
			String name = "teatro" + Integer.toString(i);
			theaterNamesList.add(name);
		}
	}

	private void shutDownServerRegistry(boolean failure, boolean force) throws RemoteException {
		int succesful = 0;

		if (!failure)
			try {
				registry.unbind(dbRegistryName);
				UnicastRemoteObject.unexportObject(this, force);
			} catch (NotBoundException e) {
				succesful = -1;
			} catch (AccessException e) {
				succesful = -1;
			} catch (UnmarshalException e) {
				System.out.println(e.detail.getMessage());
				succesful = -1;
			} catch (RemoteException e) {
				System.out.println(e.detail.getMessage());
				succesful = -1;
			}
		else {
			try {
				registry.unbind(dbRegistryName);
				registry.rebind(dbRegistryName + "temp", this);
			} catch (RemoteException e) {
				System.out.println("Error: failure mode rebind could not be completed");
				throw e;
			} catch (NotBoundException e) {
				succesful = -1;
			}
		}

		// System.exit(succesful);
	}

	private void restartServerRegistry() throws RemoteException {
		try {
			registry.unbind(dbRegistryName + "temp");
			registry.rebind(dbRegistryName, this);
		} catch (RemoteException e) {
			throw e;
		} catch (NotBoundException e) {
		}
	}

	private void cleanCache() {
		Long currentTime = System.currentTimeMillis();
		Iterator it = cache.entrySet().iterator();

		try {
			while (it.hasNext()) {
				Map.Entry entry = (Map.Entry) it.next();
				CachedTeatro cachedTeatro = (CachedTeatro) entry.getValue();
				if ((currentTime - cachedTeatro.getTimeUpdated()) > (CACHE_TIME_LIMIT * 1000)) {

					if (cachedTeatro.isAltered())
						writeTheater(cachedTeatro.getTeatro(), cachedTeatro.isInReplic());

					//System.out.println("cacheclean");
					it.remove();
					continue;
				}
				if ((currentTime - cachedTeatro.getTimeCreated()) > (CACHE_MAX_AGE * 1000)) {

					if (cachedTeatro.isAltered())
						writeTheater(cachedTeatro.getTeatro(), cachedTeatro.isInReplic());

					//System.out.println("cacheclean2");
					it.remove();

				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public void loadDatabase(ServerInfo provider, ServerInfo providerReplic, ServerInfo coordinator, Range mainRange, Range coordinatorRange, Range replicRange, int shardingLevel) throws RemoteException{
		boolean result = true;
		
		System.out.println("Received database inicialization request with level " + shardingLevel );
		
		switch(shardingLevel){
		case DBmaster.SHARDING_LEVEL_0:
			
			System.out.println("Loading Database....");
			
			// generate theaters
			for (int i = mainRange.getMin(); i <= mainRange.getMax(); i++)
				genTheater(i, rows, columns, fileOverride);
			
			////////////////////////////////////////////PRINT INFO/////////////////////////////////////////////////
    		System.out.println(eol + "New range was defined: ");
    		System.out.println("Main Range: " + mainRange.getMin() + " to " + mainRange.getMax());
			
    		System.out.println();
    		//////////////////////////////////////////////////////////////////////////////////////////////////
    		
			break;
			
		case DBmaster.SHARDING_LEVEL_1:{
			
			System.out.println("Loading Database from DB-" + provider.getId()
					+ " with address: " + provider.getAddress() + " "
					+ provider.getPort() + " ...." + eol);
			
			Registry targetRegistry;
			ReceiveMessageInterface dbServer;
			String dbRegistryName = provider.getBindName();
			
	        try {
				targetRegistry = LocateRegistry.getRegistry(provider.getAddress(), provider.getPort());
	            dbServer = (ReceiveMessageInterface) (targetRegistry.lookup(dbRegistryName));
	            
	            for (int i = mainRange.getMin(); i != (mainRange.getMax() + 1); i++){
	            	
	            	//obtem os seus dados a partir do provider
	            	Teatro teatro = dbServer.getTheater("teatro" + Integer.toString(i));
	            	writeTheater(teatro, false);
	            	
	            	//Escreve na replica do provider os seus dados
	            	dbServer.writeTheater(teatro, true);
	            	
	            	//remove os seus dados dos dados principais do provider
	            	dbServer.removeTheater(teatro.getNome(), false);
	            	
	            	if(i == numtheaters && numtheaters != mainRange.getMax())
	            		i = 0;
	            }
	            
	            //escreve na sua replica os dados do provider
	            for (int i = coordinatorRange.getMin(); i != (coordinatorRange.getMax() + 1); i++){
	            	
	            	Teatro teatro = dbServer.getTheater("teatro" + Integer.toString(i));
	            	writeTheater(teatro, true);
	            	
	            	if(i == numtheaters && numtheaters != coordinatorRange.getMax())
	            		i = 0;
	            }
	            
	            idCoordinator = coordinatorRange.getResponsable();
	            idReplic = replicRange.getResponsable();
	            
	            this.serverRange = mainRange;
	            this.coordinatorRange = coordinatorRange;
	            this.replicRange = replicRange;
	            dbServer.setNewRange(coordinatorRange, mainRange, provider.getReplicRange());
	            
	            ////////////////////////////////////////////PRINT INFO/////////////////////////////////////////////////
	    		System.out.println(eol + "New range was defined: ");
	    		System.out.println("Main Range: " + mainRange.getMin() + " to " + mainRange.getMax());
	    		
	    		if(this.coordinatorRange != null){
	    			System.out.println("Coordinator ID: " + coordinatorRange.getResponsable());
	    			System.out.println("Coordinator Range: " + coordinatorRange.getMin() + " to " + coordinatorRange.getMax());
	    		}else
	    			System.out.println("Coordinator Range: not defined");
	    		
	    		if(this.replicRange != null){
	    			System.out.println("Replic ID: " + replicRange.getResponsable());
	    			System.out.println("Replic Range: " + replicRange.getMin() + " to " + replicRange.getMax());
	    		}else
	    			System.out.println("Replic Range: not defined");
	            
	            System.out.println();
	            //////////////////////////////////////////////////////////////////////////////////////////////////
	    		
	        } catch (RemoteException e) {
	        	result = false;
	        	System.out.println("Error: Loading Database");
	        	e.printStackTrace();
	        } catch (NotBoundException e) {
	        	result = false;
	        	System.out.println("Error: Loading Database");
	        	e.printStackTrace();
	        }
			
			break;
		}
		case DBmaster.SHARDING_LEVEL_2:{
			
			System.out.println("Loading Database from DB-" + provider.getId()
					+ " with address: " + provider.getAddress() + " "
					+ provider.getPort() + " ....");
			
			System.out.println("and from Provider Replica " + providerReplic.getId()
					+ " with address: " + providerReplic.getAddress() + " "
					+ providerReplic.getPort() + " ....");
			
			Registry targetRegistry;
			Registry targetRegistry2;
			Registry targetRegistry3;
			ReceiveMessageInterface dbServer;
			ReceiveMessageInterface dbServer2;
			ReceiveMessageInterface dbServer3;
			String dbRegistryName = provider.getBindName();
			String dbRegistryName2 = providerReplic.getBindName();
			String dbRegistryName3 = coordinator.getBindName();
			
	        try {
				targetRegistry = LocateRegistry.getRegistry(provider.getAddress(), provider.getPort());
	            dbServer = (ReceiveMessageInterface) (targetRegistry.lookup(dbRegistryName));
	            
				targetRegistry2 = LocateRegistry.getRegistry(providerReplic.getAddress(), providerReplic.getPort());
	            dbServer2 = (ReceiveMessageInterface) (targetRegistry2.lookup(dbRegistryName2));
	            
				targetRegistry3 = LocateRegistry.getRegistry(coordinator.getAddress(), coordinator.getPort());
	            dbServer3 = (ReceiveMessageInterface) (targetRegistry3.lookup(dbRegistryName3));
	            
	            for (int i = mainRange.getMin(); i != (mainRange.getMax() + 1); i++){
	            	
	            	//obtem os seus dados a partir do provider
	            	Teatro teatro = dbServer.getTheater("teatro" + Integer.toString(i));
	            	writeTheater(teatro, false);

	            	//Escreve na replica do provider os seus dados
	            	dbServer.writeTheater(teatro, true);
	            	
	            	//remove os seus dados dos dados principais do provider
	            	dbServer.removeTheater(teatro.getNome(), false);
	            	
	            	//remove os seus dados da replica do servidor que replica o provider
	            	dbServer2.removeTheater(teatro.getNome(), true);
	            	
	            	
	            	if(i == numtheaters && numtheaters != mainRange.getMax())
	            		i = 0;
	            }
	            
	            
	            for (int i = coordinatorRange.getMin(); i != (coordinatorRange.getMax() + 1); i++){
	            	//escreve na sua replica os dados do coordenador do provider
	            	//Teatro teatro = dbServer2.getTheater("teatro" + Integer.toString(i));
	            	Teatro teatro = dbServer.getTheaterInReplic("teatro" + Integer.toString(i));
	            	writeTheater(teatro, true);
	            	
	            	//remove os dados do coordenador do provider na replica do provider
	            	dbServer.removeTheater(teatro.getNome(), true);
	            	
	            	if(i == numtheaters && numtheaters != coordinatorRange.getMax())
	            		i = 0;
	            }
	            
	            
	            idCoordinator = coordinatorRange.getResponsable();
	            idReplic = replicRange.getResponsable();
	            
	            this.serverRange = mainRange;
	            this.coordinatorRange = coordinatorRange;
	            this.replicRange = replicRange;
	            
	            dbServer.setNewRange(provider.getMainRange(), provider.getCoordinatorRange(), provider.getReplicRange());
	            dbServer2.setNewRange(providerReplic.getMainRange(), providerReplic.getCoordinatorRange(), providerReplic.getReplicRange());
	            
	            //Evita que o pedido seja feito duas vezes sem necessidade 
	            //(ocorre na entrada da terceira DB, altura em que o cordenador e replica do provider são o mesmo)
	            if(coordinator.getId() != providerReplic.getId())
	            	dbServer3.setNewRange(coordinator.getMainRange(), coordinator.getCoordinatorRange(), coordinator.getReplicRange());
	            
	            ////////////////////////////////////////////PRINT INFO/////////////////////////////////////////////////
	    		System.out.println( eol + "New range was defined: ");
	    		System.out.println("Main Range: " + mainRange.getMin() + " to " + mainRange.getMax());
	    		
	    		if(this.coordinatorRange != null){
	    			System.out.println("Coordinator ID: " + coordinatorRange.getResponsable());
	    			System.out.println("Coordinator Range: " + coordinatorRange.getMin() + " to " + coordinatorRange.getMax());
	    		}else
	    			System.out.println("Coordinator Range: not defined");
	    		
	    		if(this.replicRange != null){
	    			System.out.println("Replic ID: " + replicRange.getResponsable());
	    			System.out.println("Replic Range: " + replicRange.getMin() + " to " + replicRange.getMax());
	    		}else
	    			System.out.println("Replic Range: not defined");
	            
	    		System.out.println();
	    		//////////////////////////////////////////////////////////////////////////////////////////////////	
	    		
	        } catch (RemoteException e) {
	        	result = false;
	        	System.out.println("Error: Loading Database");
	        	e.printStackTrace();
	        } catch (NotBoundException e) {
	        	result = false;
	        	System.out.println("Error: Loading Database");
	        	e.printStackTrace();
	        }
			
			break;
		}
		default:
			
			break;
				
		}
		
		sendReady();

	}
	
	
	public void ReorganizeDatabase(ServerInfo departedCoordinator, ServerInfo replic, Range mainRange, Range coordinatorRange, Range replicRange, int removalLevel) throws RemoteException{
		boolean result = true;
		System.out.println("Received database reorganization request with level " + removalLevel);
		
		switch(removalLevel){
		
		case DBmaster.REMOVAL_LEVEL_2:

			System.out.println("Reorganizing Database . . .");
			
			//Adds the replicRange to the mainRange and cleans the replic storage
			for(int i=this.coordinatorRange.getMin(); i != (this.coordinatorRange.getMax() + 1); i++){
				writeTheater(getTheaterInReplic("teatro" + i), false);
				removeTheater("teatro" + i, true);
				
				if(i == numtheaters && numtheaters != this.coordinatorRange.getMax())
					i = 0;
			}
			
			this.idReplic = 0;
			this.idCoordinator = 0;
			
			this.serverRange = mainRange;
			this.replicRange = null;
			this.coordinatorRange = null;			
			
			////////////////////////////////////////////PRINT INFO/////////////////////////////////////////////////
    		System.out.println(eol + "New range was defined: ");
    		System.out.println("Main Range: " + mainRange.getMin() + " to " + mainRange.getMax());
			
    		System.out.println();
    		//////////////////////////////////////////////////////////////////////////////////////////////////

			
			break;
		case DBmaster.REMOVAL_LEVEL_3:
			
			System.out.println("Reorganizing Database in coordination with " + "DB-" +departedCoordinator.getId()
					+ " with address: " + departedCoordinator.getAddress() + " "
					+ departedCoordinator.getPort() + " ....");
			
			if(replic.getId() != departedCoordinator.getId())
				System.out.println("and with " + "DB-" +replic.getId()
						+ " with address: " + replic.getAddress() + " "
						+ replic.getPort() + " ....");
			
			Registry targetRegistry;
			Registry targetRegistry2;
			ReceiveMessageInterface dbServer;
			ReceiveMessageInterface dbServer2;
			String dbRegistryName = replic.getBindName();
			String dbRegistryName2 = departedCoordinator.getBindName();
			
	        try {
				targetRegistry = LocateRegistry.getRegistry(replic.getAddress(), replic.getPort());
	            dbServer = (ReceiveMessageInterface) (targetRegistry.lookup(dbRegistryName));
	            
				targetRegistry2 = LocateRegistry.getRegistry(departedCoordinator.getAddress(), departedCoordinator.getPort());
	            dbServer2 = (ReceiveMessageInterface) (targetRegistry2.lookup(dbRegistryName2));
	            	

	            //Adds the replicRange to the mainRange of this server and the replic server
	            //Cleans the replic storage
				for(int i=this.coordinatorRange.getMin(); i != (this.coordinatorRange.getMax() + 1); i++){
					Teatro teatro = getTheaterInReplic("teatro" + i);
					
					writeTheater(teatro, false);
					dbServer.writeTheater(teatro, true);
					
					removeTheater("teatro" + i, true);
					
					if(i == numtheaters && numtheaters != this.coordinatorRange.getMax())
						i = 0;
				}
				
				//Stores a replic of the coordinator of the departed server
				for(int i=departedCoordinator.getMainRange().getMin(); i != (departedCoordinator.getMainRange().getMax() + 1); i++){
					Teatro teatro = dbServer2.getTheater("teatro" + i);
					writeTheater(teatro, true);

					if(i == numtheaters && numtheaters != departedCoordinator.getMainRange().getMax())
						i = 0;
				}
	            
				this.idReplic = replicRange.getResponsable();
				this.idCoordinator = coordinatorRange.getResponsable();
				
				this.serverRange = mainRange;
				this.replicRange = replicRange;
				this.coordinatorRange = coordinatorRange;
				
				dbServer.setNewRange(replic.getMainRange(), replic.getCoordinatorRange(), replic.getReplicRange());
				
				if(replic.getId() != departedCoordinator.getId())
					dbServer2.setNewRange(departedCoordinator.getMainRange(), departedCoordinator.getCoordinatorRange(), departedCoordinator.getReplicRange());
	            
				////////////////////////////////////////////PRINT INFO/////////////////////////////////////////////////
	    		System.out.println( eol + "New range was defined: ");
	    		System.out.println("Main Range: " + mainRange.getMin() + " to " + mainRange.getMax());
	    		
	    		if(this.coordinatorRange != null){
	    			System.out.println("Coordinator ID: " + coordinatorRange.getResponsable());
	    			System.out.println("Coordinator Range: " + coordinatorRange.getMin() + " to " + coordinatorRange.getMax());
	    		}else
	    			System.out.println("Coordinator Range: not defined");
	    		
	    		if(this.replicRange != null){
	    			System.out.println("Replic ID: " + replicRange.getResponsable());
	    			System.out.println("Replic Range: " + replicRange.getMin() + " to " + replicRange.getMax());
	    		}else
	    			System.out.println("Replic Range: not defined");
	            
	    		System.out.println();
	    		//////////////////////////////////////////////////////////////////////////////////////////////////
				
	        } catch (RemoteException e) {
	        	result = false;
	        	System.out.println("Error: Reorganizing Database");
	        	e.printStackTrace();
	        } catch (NotBoundException e) {
	        	result = false;
	        	System.out.println("Error: Reorganizing Database");
	        	e.printStackTrace();
	        }
			
			
			break;			
		default:
			
			break;
			
		}
		
		sendReady();
	}
	
	
	public void setNewRange(Range mainRange, Range coordinatorRange, Range replicRange) throws RemoteException{
		serverRange = mainRange;
		this.replicRange = replicRange;
		this.coordinatorRange = coordinatorRange;
		
		System.out.println(eol + "NEW RANGE WAS DEFINED: ");
		System.out.println("Main Range: " + mainRange.getMin() + " to " + mainRange.getMax());
		
		if(this.coordinatorRange != null){
			idCoordinator = coordinatorRange.getResponsable();
			System.out.println("Coordinator ID: " + coordinatorRange.getResponsable());
			System.out.println("Coordinator Range: " + coordinatorRange.getMin() + " to " + coordinatorRange.getMax());
		}else
			System.out.println("Coordinator Range: not defined");
		
		if(this.replicRange != null){
			idReplic = replicRange.getResponsable();
			System.out.println("Replic ID: " + replicRange.getResponsable());
			System.out.println("Replic Range: " + replicRange.getMin() + " to " + replicRange.getMax());
		}else
			System.out.println("Replic Range: not defined");
		
		System.out.println();
		
		sendReady();
	}
	
	public void setLeaderInfo(ServerInfo leaderInfo) throws RemoteException{
		this.leaderInfo = leaderInfo;
	}
	
	private void sendReady(){
		
		boolean result = true;
		Registry targetRegistry;
		primaryDBInterface dbMaster;
		String dbRegistryName = leaderInfo.getBindName();
		
		try {
			targetRegistry = LocateRegistry.getRegistry(leaderInfo.getAddress(), leaderInfo.getPort());
            dbMaster = (primaryDBInterface) (targetRegistry.lookup(dbRegistryName));
            
            System.out.println("notifying master of ready status");
            dbMaster.setReady(id);
            
        } catch (RemoteException e) {
        	result = false;
        	System.out.println("Error: failed to notify master of ready status");
        	e.printStackTrace();
        } catch (NotBoundException e) {
        	result = false;
        	System.out.println("Error: failed to notify master of ready status");
        	e.printStackTrace();
        }
	}
	
	public void run() {
		synchronized (this) {
			if (!cache.isEmpty())
				cleanCache();
		}
	}
	
	private byte[] compressSerializedTheater(Teatro teatro){
		
		byte[] buf = null;
		
		try{
			ObjectOutputStream out;
			
			// Serialize data object to a byte array
			ByteArrayOutputStream bos = new ByteArrayOutputStream() ;
			GZIPOutputStream gz = new GZIPOutputStream(bos);
			GZIPOutputStream gz2 = new GZIPOutputStream(gz);
			out = new ObjectOutputStream(gz2) ;
			out.writeObject(teatro);
			out.close();
	
			// Get the bytes of the serialized object
			buf = bos.toByteArray();
		}catch(IOException e){
			System.out.println("ERROR during teather compression!");
		}
		
		return buf;
	}
	
	private byte[] compressSerializedListOfTheaters(ArrayList<String> listTheaters){
		
		byte[] buf = null;
		
		try{
			ObjectOutputStream out;
			
			// Serialize data object to a byte array
			ByteArrayOutputStream bos = new ByteArrayOutputStream() ;
			GZIPOutputStream gz = new GZIPOutputStream(bos);
			GZIPOutputStream gz2 = new GZIPOutputStream(gz);
			out = new ObjectOutputStream(gz2) ;
			out.writeObject(listTheaters);
			out.close();
	
			// Get the bytes of the serialized object
			buf = bos.toByteArray();
		}catch(IOException e){
			System.out.println("ERROR during teather compression!");
		}
		
		return buf;
	}
	
	private Teatro decompressTheater(byte[] buf){
		Teatro teatro = null;
		
		// Get the bytes of the serialized object
		byte[] buf2 = decompress(buf);
		byte[] buf3 = decompress(buf2);
		
		try{
			ByteArrayInputStream bis= new ByteArrayInputStream(buf3);
			ObjectInputStream ois = new ObjectInputStream(bis);
			
			try {
				teatro = (Teatro)ois.readObject();
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			}
		}catch (IOException e){
			System.out.println("ERROR during teather decompression!");
		}
		
		return teatro;
	}
	
    private static byte[] decompress(byte[] contentBytes){
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try{
            IOUtils.copy(new GZIPInputStream(new ByteArrayInputStream(contentBytes)), out);
        } catch(IOException e){
            throw new RuntimeException(e);
        }
        return out.toByteArray();
    }

}
